/**
 * Angular JS slider directive
 *
 * (c) Rafal Zajac <rzajac@gmail.com>
 * http://github.com/rzajac/angularjs-slider
 *
 * Licensed under the MIT license
 */
/*jslint unparam: true */
/*global angular: false, console: false, define, module */
;(function(root, factory) {
  'use strict'
  /* istanbul ignore next */
  if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module.
    define(['angular'], factory)
  } else if (typeof module === 'object' && module.exports) {
    // Node. Does not work with strict CommonJS, but
    // only CommonJS-like environments that support module.exports,
    // like Node.
    // to support bundler like browserify
    var angularObj = angular || require('angular')
    if ((!angularObj || !angularObj.module) && typeof angular != 'undefined') {
      angularObj = angular
    }
    module.exports = factory(angularObj)
  } else {
    // Browser globals (root is window)
    factory(root.angular)
  }
})(this, function(angular) {
  'use strict'
  var module = angular
    .module('rzSlider', [])
    .factory('RzSliderOptions', function() {
      var defaultOptions = {
        floor: 0,
        ceil: null, //defaults to rz-slider-model
        step: 1,
        precision: 0,
        minRange: null,
        maxRange: null,
        restrictedRange: null,
        skipRestrictedRangesWithArrowKeys: null,
        pushRange: false,
        minLimit: null,
        maxLimit: null,
        id: null,
        translate: null,
        getLegend: null,
        stepsArray: null,
        bindIndexForStepsArray: false,
        draggableRange: false,
        draggableRangeOnly: false,
        showSelectionBar: false,
        showSelectionBarEnd: false,
        showSelectionBarFromValue: null,
        showOuterSelectionBars: false,
        hidePointerLabels: false,
        hideLimitLabels: false,
        autoHideLimitLabels: true,
        readOnly: false,
        disabled: false,
        interval: 350,
        showTicks: false,
        showTicksValues: false,
        ticksArray: null,
        ticksTooltip: null,
        ticksValuesTooltip: null,
        vertical: false,
        getSelectionBarColor: null,
        getTickColor: null,
        getPointerColor: null,
        keyboardSupport: true,
        scale: 1,
        enforceStep: true,
        enforceRange: false,
        noSwitching: false,
        onlyBindHandles: false,
        disableAnimation: false,
        onStart: null,
        onChange: null,
        onEnd: null,
        rightToLeft: false,
        reversedControls: false,
        boundPointerLabels: true,
        mergeRangeLabelsIfSame: false,
        labelOverlapSeparator: ' - ',
        customTemplateScope: null,
        logScale: false,
        customValueToPosition: null,
        customPositionToValue: null,
        selectionBarGradient: null,
        ariaLabel: null,
        ariaLabelledBy: null,
        ariaLabelHigh: null,
        ariaLabelledByHigh: null,
      }
      var globalOptions = {}

      var factory = {}
      /**
       * `options({})` allows global configuration of all sliders in the
       * application.
       *
       *   var app = angular.module( 'App', ['rzSlider'], function( RzSliderOptions ) {
       *     // show ticks for all sliders
       *     RzSliderOptions.options( { showTicks: true } );
       *   });
       */
      factory.options = function(value) {
        angular.extend(globalOptions, value)
      }

      factory.getOptions = function(options) {
        return angular.extend({}, defaultOptions, globalOptions, options)
      }

      return factory
    })
    .factory('rzThrottle', function($timeout) {
      /**
       * rzThrottle
       *
       * Taken from underscore project
       *
       * @param {Function} func
       * @param {number} wait
       * @param {ThrottleOptions} options
       * @returns {Function}
       */
      return function(func, wait, options) {
        'use strict'
        /* istanbul ignore next */
        var getTime =
          Date.now ||
          function() {
            return new Date().getTime()
          }
        var context, args, result
        var timeout = null
        var previous = 0
        options = options || {}
        var later = function() {
          previous = getTime()
          timeout = null
          result = func.apply(context, args)
          context = args = null
        }
        return function() {
          var now = getTime()
          var remaining = wait - (now - previous)
          context = this
          args = arguments
          if (remaining <= 0) {
            $timeout.cancel(timeout)
            timeout = null
            previous = now
            result = func.apply(context, args)
            context = args = null
          } else if (!timeout && options.trailing !== false) {
            timeout = $timeout(later, remaining)
          }
          return result
        }
      }
    })
    .factory('RzSlider', function(
      $timeout,
      $document,
      $window,
      $compile,
      RzSliderOptions,
      rzThrottle
    ) {
      'use strict'

      /**
       * Slider
       *
       * @param {ngScope} scope            The AngularJS scope
       * @param {Element} sliderElem The slider directive element wrapped in jqLite
       * @constructor
       */
      var Slider = function(scope, sliderElem) {
        /**
         * The slider's scope
         *
         * @type {ngScope}
         */
        this.scope = scope

        /**
         * The slider inner low value (linked to rzSliderModel)
         * @type {number}
         */
        this.lowValue = 0

        /**
         * The slider inner high value (linked to rzSliderHigh)
         * @type {number}
         */
        this.highValue = 0

        /**
         * Slider element wrapped in jqLite
         *
         * @type {jqLite}
         */
        this.sliderElem = sliderElem

        /**
         * Slider type
         *
         * @type {boolean} Set to true for range slider
         */
        this.range =
          this.scope.rzSliderModel !== undefined &&
          this.scope.rzSliderHigh !== undefined

        /**
         * Values recorded when first dragging the bar
         *
         * @type {Object}
         */
        this.dragging = {
          active: false,
          value: 0,
          difference: 0,
          position: 0,
          lowLimit: 0,
          highLimit: 0,
        }

        /**
         * property that handle position (defaults to left for horizontal)
         * @type {string}
         */
        this.positionProperty = 'left'

        /**
         * property that handle dimension (defaults to width for horizontal)
         * @type {string}
         */
        this.dimensionProperty = 'width'

        /**
         * Half of the width or height of the slider handles
         *
         * @type {number}
         */
        this.handleHalfDim = 0

        /**
         * Maximum position the slider handle can have
         *
         * @type {number}
         */
        this.maxPos = 0

        /**
         * Precision
         *
         * @type {number}
         */
        this.precision = 0

        /**
         * Step
         *
         * @type {number}
         */
        this.step = 1

        /**
         * The name of the handle we are currently tracking
         *
         * @type {string}
         */
        this.tracking = ''

        /**
         * Minimum value (floor) of the model
         *
         * @type {number}
         */
        this.minValue = 0

        /**
         * Maximum value (ceiling) of the model
         *
         * @type {number}
         */
        this.maxValue = 0

        /**
         * The delta between min and max value
         *
         * @type {number}
         */
        this.valueRange = 0

        /**
         * If showTicks/showTicksValues options are number.
         * In this case, ticks values should be displayed below the slider.
         * @type {boolean}
         */
        this.intermediateTicks = false

        /**
         * Set to true if init method already executed
         *
         * @type {boolean}
         */
        this.initHasRun = false

        /**
         * Used to call onStart on the first keydown event
         *
         * @type {boolean}
         */
        this.firstKeyDown = false

        /**
         * Internal flag to prevent watchers to be called when the sliders value are modified internally.
         * @type {boolean}
         */
        this.internalChange = false

        /**
         * Internal flag to keep track of the visibility of combo label
         * @type {boolean}
         */
        this.cmbLabelShown = false

        /**
         * Internal variable to keep track of the focus element
         */
        this.currentFocusElement = null

        /**
         * Internal variable to know if we are already moving
         */
        this.moving = false

        // Slider DOM elements wrapped in jqLite
        this.fullBar = null // The whole slider bar
        this.selBar = null // Highlight between two handles
        this.minH = null // Left slider handle
        this.maxH = null // Right slider handle
        this.flrLab = null // Floor label
        this.ceilLab = null // Ceiling label
        this.minLab = null // Label above the low value
        this.maxLab = null // Label above the high value
        this.cmbLab = null // Combined label
        this.ticks = null // The ticks

        // Initialize slider
        this.init()
      }

      // Add instance methods
      Slider.prototype = {
        /**
         * Initialize slider
         *
         * @returns {undefined}
         */
        init: function() {
          var thrLow,
            thrHigh,
            self = this

          var calcDimFn = function() {
            self.calcViewDimensions()
          }

          this.applyOptions()
          this.syncLowValue()
          if (this.range) this.syncHighValue()
          this.initElemHandles()
          this.manageElementsStyle()
          this.setDisabledState()
          this.calcViewDimensions()
          this.setMinAndMax()
          this.updateRestrictionBar()
          this.addAccessibility()
          this.updateCeilLab()
          this.updateFloorLab()
          this.initHandles()
          this.manageEventsBindings()

          // Recalculate slider view dimensions
          this.scope.$on('reCalcViewDimensions', calcDimFn)

          // Recalculate stuff if view port dimensions have changed
          angular.element($window).on('resize', calcDimFn)

          this.initHasRun = true

          if (this.options.disableAnimation) {
            this.sliderElem.addClass('noanimate')
          }

          // Watch for changes to the model
          thrLow = rzThrottle(function() {
            self.onLowHandleChange()
          }, self.options.interval)

          thrHigh = rzThrottle(function() {
            self.onHighHandleChange()
          }, self.options.interval)

          this.scope.$on('rzSliderForceRender', function() {
            self.resetLabelsValue()
            thrLow()
            if (self.range) {
              thrHigh()
            }
            self.resetSlider()
          })

          // Watchers (order is important because in case of simultaneous change,
          // watchers will be called in the same order)
          this.scope.$watchCollection('rzSliderOptions()', function(
            newValue,
            oldValue
          ) {
            if (newValue === oldValue) return
            self.applyOptions() // need to be called before synchronizing the values
            self.syncLowValue()
            if (self.range) self.syncHighValue()
            self.resetSlider()
          })

          this.scope.$watch('rzSliderModel', function(newValue, oldValue) {
            if (self.internalChange) return
            if (newValue === oldValue) return
            thrLow()
          })

          this.scope.$watch('rzSliderHigh', function(newValue, oldValue) {
            if (self.internalChange) return
            if (newValue === oldValue) return
            if (newValue != null) thrHigh()
            if (
              (self.range && newValue == null) ||
              (!self.range && newValue != null)
            ) {
              self.applyOptions()
              self.resetSlider()
            }
          })

          this.scope.$on('$destroy', function() {
            self.unbindEvents()
            angular.element($window).off('resize', calcDimFn)
            self.currentFocusElement = null
          })
        },

        findStepIndex: function(modelValue) {
          var index = 0
          for (var i = 0; i < this.options.stepsArray.length; i++) {
            var step = this.options.stepsArray[i]
            if (step === modelValue) {
              index = i
              break
            } else if (angular.isDate(step)) {
              if (step.getTime() === modelValue.getTime()) {
                index = i
                break
              }
            } else if (angular.isObject(step)) {
              if (
                (angular.isDate(step.value) &&
                  step.value.getTime() === modelValue.getTime()) ||
                step.value === modelValue
              ) {
                index = i
                break
              }
            }
          }
          return index
        },

        syncLowValue: function() {
          if (this.options.stepsArray) {
            if (!this.options.bindIndexForStepsArray)
              this.lowValue = this.findStepIndex(this.scope.rzSliderModel)
            else this.lowValue = this.scope.rzSliderModel
          } else this.lowValue = this.scope.rzSliderModel
        },

        syncHighValue: function() {
          if (this.options.stepsArray) {
            if (!this.options.bindIndexForStepsArray)
              this.highValue = this.findStepIndex(this.scope.rzSliderHigh)
            else this.highValue = this.scope.rzSliderHigh
          } else this.highValue = this.scope.rzSliderHigh
        },

        getStepValue: function(sliderValue) {
          var step = this.options.stepsArray[sliderValue]
          if (angular.isDate(step)) return step
          if (angular.isObject(step)) return step.value
          return step
        },

        applyLowValue: function() {
          if (this.options.stepsArray) {
            if (!this.options.bindIndexForStepsArray)
              this.scope.rzSliderModel = this.getStepValue(this.lowValue)
            else this.scope.rzSliderModel = this.lowValue
          } else this.scope.rzSliderModel = this.lowValue
        },

        applyHighValue: function() {
          if (this.options.stepsArray) {
            if (!this.options.bindIndexForStepsArray)
              this.scope.rzSliderHigh = this.getStepValue(this.highValue)
            else this.scope.rzSliderHigh = this.highValue
          } else this.scope.rzSliderHigh = this.highValue
        },

        /*
         * Reflow the slider when the low handle changes (called with throttle)
         */
        onLowHandleChange: function() {
          this.syncLowValue()
          if (this.range) this.syncHighValue()
          this.setMinAndMax()
          this.updateLowHandle(this.valueToPosition(this.lowValue))
          this.updateSelectionBar()
          this.updateTicksScale()
          this.updateAriaAttributes()
          if (this.range) {
            this.updateCmbLabel()
          }
        },

        /*
         * Reflow the slider when the high handle changes (called with throttle)
         */
        onHighHandleChange: function() {
          this.syncLowValue()
          this.syncHighValue()
          this.setMinAndMax()
          this.updateHighHandle(this.valueToPosition(this.highValue))
          this.updateSelectionBar()
          this.updateTicksScale()
          this.updateCmbLabel()
          this.updateAriaAttributes()
        },

        /**
         * Read the user options and apply them to the slider model
         */
        applyOptions: function() {
          var sliderOptions
          if (this.scope.rzSliderOptions)
            sliderOptions = this.scope.rzSliderOptions()
          else sliderOptions = {}

          this.options = RzSliderOptions.getOptions(sliderOptions)

          if (this.options.step <= 0) this.options.step = 1

          this.range =
            this.scope.rzSliderModel !== undefined &&
            this.scope.rzSliderHigh !== undefined
          this.options.draggableRange =
            this.range && this.options.draggableRange
          this.options.draggableRangeOnly =
            this.range && this.options.draggableRangeOnly
          if (this.options.draggableRangeOnly) {
            this.options.draggableRange = true
          }

          this.options.showTicks =
            this.options.showTicks ||
            this.options.showTicksValues ||
            !!this.options.ticksArray
          this.scope.showTicks = this.options.showTicks //scope is used in the template
          if (
            angular.isNumber(this.options.showTicks) ||
            this.options.ticksArray
          )
            this.intermediateTicks = true

          this.options.showSelectionBar =
            this.options.showSelectionBar ||
            this.options.showSelectionBarEnd ||
            this.options.showSelectionBarFromValue !== null

          if (this.options.stepsArray) {
            this.parseStepsArray()
          } else {
            if (this.options.translate) this.customTrFn = this.options.translate
            else
              this.customTrFn = function(value) {
                return String(value)
              }

            this.getLegend = this.options.getLegend
          }

          if (this.options.vertical) {
            this.positionProperty = 'bottom'
            this.dimensionProperty = 'height'
          } else {
            this.positionProperty = 'left'
            this.dimensionProperty = 'width'
          }

          if (this.options.customTemplateScope)
            this.scope.custom = this.options.customTemplateScope
        },

        parseStepsArray: function() {
          this.options.floor = 0
          this.options.ceil = this.options.stepsArray.length - 1
          this.options.step = 1

          if (this.options.translate) {
            this.customTrFn = this.options.translate
          } else {
            this.customTrFn = function(modelValue) {
              if (this.options.bindIndexForStepsArray)
                return this.getStepValue(modelValue)
              return modelValue
            }
          }

          this.getLegend = function(index) {
            var step = this.options.stepsArray[index]
            if (angular.isObject(step)) return step.legend
            return null
          }
        },

        /**
         * Resets slider
         *
         * @returns {undefined}
         */
        resetSlider: function() {
          this.resetLabelsValue()
          this.manageElementsStyle()
          this.addAccessibility()
          this.setMinAndMax()
          this.updateCeilLab()
          this.updateFloorLab()
          this.unbindEvents()
          this.manageEventsBindings()
          this.setDisabledState()
          this.calcViewDimensions()
          this.updateRestrictionBar()
          this.refocusPointerIfNeeded()
        },

        refocusPointerIfNeeded: function() {
          if (this.currentFocusElement) {
            this.onPointerFocus(
              this.currentFocusElement.pointer,
              this.currentFocusElement.ref
            )
            this.focusElement(this.currentFocusElement.pointer)
          }
        },

        /**
         * Check if the restrictedRange option using multiple or not
         *
         * Run only once during initialization and only in case 4
         *
         * @returns {undefined}
         */

        ensureRestrictedBarIsArray: function(elem) {
          var jElem = angular.element(elem)
          this.restrictedBar = []
          if (this.options.restrictedRange) {
            // this.options.restrictedRange converting to an array even if it's not entered as array.
            this.options.restrictedRange = !Array.isArray(
              this.options.restrictedRange
            )
              ? [this.options.restrictedRange]
              : this.options.restrictedRange
            this.restrictedBar[0] = jElem
            var mainDiv = elem.parentElement
            for (var i = 1; i < this.options.restrictedRange.length; i++) {
              var sp = document.createElement('span')
              sp.setAttribute('class', 'rz-bar-wrapper')
              sp.innerHTML =
                '<span class="rz-bar rz-restricted" ng-style="restrictionStyle"></span>'
              mainDiv.appendChild(sp)
              this.restrictedBar[i] = angular.element(sp)
            }
          } else {
            elem.style.visibility = 'hidden'
            this.restrictedBar = null
          }
        },

        /**
         * Set the slider children to variables for easy access
         *
         * Run only once during initialization
         *
         * @returns {undefined}
         */
        initElemHandles: function() {
          // Assign all slider elements to object properties for easy access
          angular.forEach(
            this.sliderElem.children(),
            function(elem, index) {
              var jElem = angular.element(elem)

              switch (index) {
                case 0:
                  this.leftOutSelBar = jElem
                  break
                case 1:
                  this.rightOutSelBar = jElem
                  break
                case 2:
                  this.fullBar = jElem
                  break
                case 3:
                  this.selBar = jElem
                  break
                case 4:
                  this.ensureRestrictedBarIsArray(elem)
                  break
                case 5:
                  this.minH = jElem
                  break
                case 6:
                  this.maxH = jElem
                  break
                case 7:
                  this.flrLab = jElem
                  break
                case 8:
                  this.ceilLab = jElem
                  break
                case 9:
                  this.minLab = jElem
                  break
                case 10:
                  this.maxLab = jElem
                  break
                case 11:
                  this.cmbLab = jElem
                  break
                case 12:
                  this.ticks = jElem
                  break
              }
            },
            this
          )

          // Initialize position cache properties
          this.selBar.rzsp = 0
          this.minH.rzsp = 0
          this.maxH.rzsp = 0
          this.flrLab.rzsp = 0
          this.ceilLab.rzsp = 0
          this.minLab.rzsp = 0
          this.maxLab.rzsp = 0
          this.cmbLab.rzsp = 0
        },

        /**
         * Update each elements style based on options
         */
        manageElementsStyle: function() {
          if (!this.range) this.maxH.css('display', 'none')
          else this.maxH.css('display', '')

          this.alwaysHide(
            this.flrLab,
            this.options.showTicksValues || this.options.hideLimitLabels
          )
          this.alwaysHide(
            this.ceilLab,
            this.options.showTicksValues || this.options.hideLimitLabels
          )

          var hideLabelsForTicks =
            this.options.showTicksValues && !this.intermediateTicks
          this.alwaysHide(
            this.minLab,
            hideLabelsForTicks || this.options.hidePointerLabels
          )
          this.alwaysHide(
            this.maxLab,
            hideLabelsForTicks || !this.range || this.options.hidePointerLabels
          )
          this.alwaysHide(
            this.cmbLab,
            hideLabelsForTicks || !this.range || this.options.hidePointerLabels
          )
          this.alwaysHide(
            this.selBar,
            !this.range && !this.options.showSelectionBar
          )
          this.alwaysHide(
            this.leftOutSelBar,
            !this.range || !this.options.showOuterSelectionBars
          )

          // this.restrictedBar is everytime an array
          for (var r in this.restrictedBar) {
            if (this.restrictedBar[r])
              this.alwaysHide(
                this.restrictedBar[r],
                !this.options.restrictedRange[r]
              )
          }

          this.alwaysHide(
            this.rightOutSelBar,
            !this.range || !this.options.showOuterSelectionBars
          )

          if (this.range && this.options.showOuterSelectionBars) {
            this.fullBar.addClass('rz-transparent')
          }

          if (this.options.vertical) {
            this.sliderElem.addClass('rz-vertical')
          } else {
            this.sliderElem.removeClass('rz-vertical')
          }

          if (this.options.draggableRange) this.selBar.addClass('rz-draggable')
          else this.selBar.removeClass('rz-draggable')

          if (this.intermediateTicks && this.options.showTicksValues)
            this.ticks.addClass('rz-ticks-values-under')
        },

        alwaysHide: function(el, hide) {
          el.rzAlwaysHide = hide
          if (hide) this.hideEl(el)
          else this.showEl(el)
        },

        /**
         * Manage the events bindings based on readOnly and disabled options
         *
         * @returns {undefined}
         */
        manageEventsBindings: function() {
          if (this.options.disabled || this.options.readOnly)
            this.unbindEvents()
          else this.bindEvents()
        },

        /**
         * Set the disabled state based on rzSliderDisabled
         *
         * @returns {undefined}
         */
        setDisabledState: function() {
          if (this.options.disabled) {
            this.sliderElem.attr('disabled', 'disabled')
          } else {
            this.sliderElem.attr('disabled', null)
          }
        },

        /**
         * Reset label values
         *
         * @return {undefined}
         */
        resetLabelsValue: function() {
          this.minLab.rzsv = undefined
          this.maxLab.rzsv = undefined
          this.flrLab.rzsv = undefined
          this.ceilLab.rzsv = undefined
          this.cmbLab.rzsv = undefined
          this.resetPosition(this.flrLab)
          this.resetPosition(this.ceilLab)
          this.resetPosition(this.cmbLab)
          this.resetPosition(this.minLab)
          this.resetPosition(this.maxLab)
        },

        /**
         * Initialize slider handles positions and labels
         *
         * Run only once during initialization and every time view port changes size
         *
         * @returns {undefined}
         */
        initHandles: function() {
          this.updateLowHandle(this.valueToPosition(this.lowValue))

          /*
         the order here is important since the selection bar should be
         updated after the high handle but before the combined label
         */
          if (this.range)
            this.updateHighHandle(this.valueToPosition(this.highValue))
          this.updateSelectionBar()
          if (this.range) this.updateCmbLabel()

          this.updateTicksScale()
        },

        /**
         * Translate value to human readable format
         *
         * @param {number|string} value
         * @param {jqLite} label
         * @param {String} which
         * @param {boolean} [useCustomTr]
         * @returns {undefined}
         */
        translateFn: function(value, label, which, useCustomTr) {
          useCustomTr = useCustomTr === undefined ? true : useCustomTr

          var valStr = '',
            getDimension = false,
            noLabelInjection = label.hasClass('no-label-injection')

          if (useCustomTr) {
            if (this.options.stepsArray && !this.options.bindIndexForStepsArray)
              value = this.getStepValue(value)
            valStr = String(this.customTrFn(value, this.options.id, which))
          } else {
            valStr = String(value)
          }

          if (
            label.rzsv === undefined ||
            label.rzsv.length !== valStr.length ||
            (label.rzsv.length > 0 && label.rzsd === 0)
          ) {
            getDimension = true
            label.rzsv = valStr
          }

          if (!noLabelInjection) {
            label.html(valStr)
          }
          this.scope[which + 'Label'] = valStr

          // Update width only when length of the label have changed
          if (getDimension) {
            this.getDimension(label)
          }
        },

        /**
         * Set maximum and minimum values for the slider and ensure the model and high
         * value match these limits
         * @returns {undefined}
         */
        setMinAndMax: function() {
          this.step = +this.options.step
          this.precision = +this.options.precision

          this.minValue = this.options.floor
          if (this.options.logScale && this.minValue === 0)
            throw Error("Can't use floor=0 with logarithmic scale")

          if (this.options.enforceStep) {
            this.lowValue = this.roundStep(this.lowValue)
            if (this.range) this.highValue = this.roundStep(this.highValue)
          }

          if (this.options.ceil != null) this.maxValue = this.options.ceil
          else
            this.maxValue = this.options.ceil = this.range
              ? this.highValue
              : this.lowValue

          if (this.options.enforceRange) {
            this.lowValue = this.sanitizeValue(this.lowValue)
            if (this.range) this.highValue = this.sanitizeValue(this.highValue)
          }

          this.applyLowValue()
          if (this.range) this.applyHighValue()

          this.valueRange = this.maxValue - this.minValue
        },

        /**
         * Adds accessibility attributes
         *
         * Run only once during initialization
         *
         * @returns {undefined}
         */
        addAccessibility: function() {
          this.minH.attr('role', 'slider')
          this.updateAriaAttributes()
          if (
            this.options.keyboardSupport &&
            !(this.options.readOnly || this.options.disabled)
          )
            this.minH.attr('tabindex', '0')
          else this.minH.attr('tabindex', '')
          if (this.options.vertical) {
            this.minH.attr('aria-orientation', 'vertical')
          } else {
            this.minH.attr('aria-orientation', 'horizontal')
          }
          if (this.options.ariaLabel)
            this.minH.attr('aria-label', this.options.ariaLabel)
          else if (this.options.ariaLabelledBy)
            this.minH.attr('aria-labelledby', this.options.ariaLabelledBy)

          if (this.range) {
            this.maxH.attr('role', 'slider')
            if (
              this.options.keyboardSupport &&
              !(this.options.readOnly || this.options.disabled)
            )
              this.maxH.attr('tabindex', '0')
            else this.maxH.attr('tabindex', '')
            if (this.options.vertical)
              this.maxH.attr('aria-orientation', 'vertical')
            else this.maxH.attr('aria-orientation', 'horizontal')
            if (this.options.ariaLabelHigh)
              this.maxH.attr('aria-label', this.options.ariaLabelHigh)
            else if (this.options.ariaLabelledByHigh)
              this.maxH.attr('aria-labelledby', this.options.ariaLabelledByHigh)
          }
        },

        /**
         * Updates aria attributes according to current values
         */
        updateAriaAttributes: function() {
          this.minH.attr({
            'aria-valuenow': this.scope.rzSliderModel,
            'aria-valuetext': this.customTrFn(
              this.scope.rzSliderModel,
              this.options.id,
              'model'
            ),
            'aria-valuemin': this.minValue,
            'aria-valuemax': this.maxValue,
          })
          if (this.range) {
            this.maxH.attr({
              'aria-valuenow': this.scope.rzSliderHigh,
              'aria-valuetext': this.customTrFn(
                this.scope.rzSliderHigh,
                this.options.id,
                'high'
              ),
              'aria-valuemin': this.minValue,
              'aria-valuemax': this.maxValue,
            })
          }
        },

        /**
         * Calculate dimensions that are dependent on view port size
         *
         * Run once during initialization and every time view port changes size.
         *
         * @returns {undefined}
         */
        calcViewDimensions: function() {
          var handleWidth = this.getDimension(this.minH)

          this.handleHalfDim = handleWidth / 2
          this.barDimension = this.getDimension(this.fullBar)

          this.maxPos = this.barDimension - handleWidth

          this.getDimension(this.sliderElem)
          this.sliderElem.rzsp = this.sliderElem[0].getBoundingClientRect()[
            this.positionProperty
          ]

          if (this.initHasRun) {
            this.updateFloorLab()
            this.updateCeilLab()
            this.initHandles()
            var self = this
            $timeout(function() {
              self.updateTicksScale()
            })
          }
        },

        /**
         * Update the ticks position
         *
         * @returns {undefined}
         */
        updateTicksScale: function() {
          if (!this.options.showTicks) return

          var ticksArray = this.options.ticksArray || this.getTicksArray(),
            translate = this.options.vertical ? 'translateY' : 'translateX',
            self = this

          if (this.options.rightToLeft) ticksArray.reverse()

          this.scope.ticks = ticksArray.map(function(value) {
            var legend = null
            if (angular.isObject(value)) {
              legend = value.legend
              value = value.value
            }

            var position = self.valueToPosition(value)

            if (self.options.vertical) position = self.maxPos - position

            var translation = translate + '(' + Math.round(position) + 'px)'
            var tick = {
              legend: legend,
              selected: self.isTickSelected(value),
              style: {
                '-webkit-transform': translation,
                '-moz-transform': translation,
                '-o-transform': translation,
                '-ms-transform': translation,
                transform: translation,
              },
            }
            if (tick.selected && self.options.getSelectionBarColor) {
              tick.style['background-color'] = self.getSelectionBarColor()
            }
            if (!tick.selected && self.options.getTickColor) {
              tick.style['background-color'] = self.getTickColor(value)
            }
            if (self.options.ticksTooltip) {
              tick.tooltip = self.options.ticksTooltip(value)
              tick.tooltipPlacement = self.options.vertical ? 'right' : 'top'
            }
            if (
              self.options.showTicksValues === true ||
              value % self.options.showTicksValues === 0
            ) {
              tick.value = self.getDisplayValue(value, 'tick-value')
              if (self.options.ticksValuesTooltip) {
                tick.valueTooltip = self.options.ticksValuesTooltip(value)
                tick.valueTooltipPlacement = self.options.vertical
                  ? 'right'
                  : 'top'
              }
            }
            if (self.getLegend) {
              legend = self.getLegend(value, self.options.id)
              if (legend) tick.legend = legend
            }
            return tick
          })
        },

        getTicksArray: function() {
          var step = this.step,
            ticksArray = []
          if (this.intermediateTicks) step = this.options.showTicks
          for (
            var value = this.minValue;
            value <= this.maxValue;
            value += step
          ) {
            ticksArray.push(value)
          }
          return ticksArray
        },

        isTickSelected: function(value) {
          if (!this.range) {
            if (this.options.showSelectionBarFromValue !== null) {
              var center = this.options.showSelectionBarFromValue
              if (
                this.lowValue > center &&
                value >= center &&
                value <= this.lowValue
              )
                return true
              else if (
                this.lowValue < center &&
                value <= center &&
                value >= this.lowValue
              )
                return true
            } else if (this.options.showSelectionBarEnd) {
              if (value >= this.lowValue) return true
            } else if (this.options.showSelectionBar && value <= this.lowValue)
              return true
          }
          if (this.range && value >= this.lowValue && value <= this.highValue)
            return true
          return false
        },

        /**
         * Update position of the floor label
         *
         * @returns {undefined}
         */
        updateFloorLab: function() {
          this.translateFn(this.minValue, this.flrLab, 'floor')
          this.getDimension(this.flrLab)
          var position = this.options.rightToLeft
            ? this.barDimension - this.flrLab.rzsd
            : 0
          this.setPosition(this.flrLab, position)
        },

        /**
         * Update position of the ceiling label
         *
         * @returns {undefined}
         */
        updateCeilLab: function() {
          this.translateFn(this.maxValue, this.ceilLab, 'ceil')
          this.getDimension(this.ceilLab)
          var position = this.options.rightToLeft
            ? 0
            : this.barDimension - this.ceilLab.rzsd
          this.setPosition(this.ceilLab, position)
        },

        /**
         * Update slider handles and label positions
         *
         * @param {string} which
         * @param {number} newPos
         */
        updateHandles: function(which, newPos) {
          if (which === 'lowValue') this.updateLowHandle(newPos)
          else this.updateHighHandle(newPos)

          this.updateSelectionBar()
          this.updateTicksScale()
          if (this.range) this.updateCmbLabel()
        },

        /**
         * Helper function to work out the position for handle labels depending on RTL or not
         *
         * @param {string} labelName maxLab or minLab
         * @param newPos
         *
         * @returns {number}
         */
        getHandleLabelPos: function(labelName, newPos) {
          var labelRzsd = this[labelName].rzsd,
            nearHandlePos = newPos - labelRzsd / 2 + this.handleHalfDim,
            endOfBarPos = this.barDimension - labelRzsd

          if (!this.options.boundPointerLabels) return nearHandlePos

          if (
            (this.options.rightToLeft && labelName === 'minLab') ||
            (!this.options.rightToLeft && labelName === 'maxLab')
          ) {
            return Math.min(nearHandlePos, endOfBarPos)
          } else {
            return Math.min(Math.max(nearHandlePos, 0), endOfBarPos)
          }
        },

        /**
         * Update low slider handle position and label
         *
         * @param {number} newPos
         * @returns {undefined}
         */
        updateLowHandle: function(newPos) {
          this.setPosition(this.minH, newPos)
          this.translateFn(this.lowValue, this.minLab, 'model')
          this.setPosition(
            this.minLab,
            this.getHandleLabelPos('minLab', newPos)
          )

          if (this.options.getPointerColor) {
            var pointercolor = this.getPointerColor('min')
            this.scope.minPointerStyle = {
              backgroundColor: pointercolor,
            }
          }

          if (this.options.autoHideLimitLabels) {
            this.shFloorCeil()
          }
        },

        /**
         * Update high slider handle position and label
         *
         * @param {number} newPos
         * @returns {undefined}
         */
        updateHighHandle: function(newPos) {
          this.setPosition(this.maxH, newPos)
          this.translateFn(this.highValue, this.maxLab, 'high')
          this.setPosition(
            this.maxLab,
            this.getHandleLabelPos('maxLab', newPos)
          )

          if (this.options.getPointerColor) {
            var pointercolor = this.getPointerColor('max')
            this.scope.maxPointerStyle = {
              backgroundColor: pointercolor,
            }
          }
          if (this.options.autoHideLimitLabels) {
            this.shFloorCeil()
          }
        },

        /**
         * Show/hide floor/ceiling label
         *
         * @returns {undefined}
         */
        shFloorCeil: function() {
          // Show based only on hideLimitLabels if pointer labels are hidden
          if (this.options.hidePointerLabels) {
            return
          }
          var flHidden = false,
            clHidden = false,
            isMinLabAtFloor = this.isLabelBelowFloorLab(this.minLab),
            isMinLabAtCeil = this.isLabelAboveCeilLab(this.minLab),
            isMaxLabAtCeil = this.isLabelAboveCeilLab(this.maxLab),
            isCmbLabAtFloor = this.isLabelBelowFloorLab(this.cmbLab),
            isCmbLabAtCeil = this.isLabelAboveCeilLab(this.cmbLab)

          if (isMinLabAtFloor) {
            flHidden = true
            this.hideEl(this.flrLab)
          } else {
            flHidden = false
            this.showEl(this.flrLab)
          }

          if (isMinLabAtCeil) {
            clHidden = true
            this.hideEl(this.ceilLab)
          } else {
            clHidden = false
            this.showEl(this.ceilLab)
          }

          if (this.range) {
            var hideCeil = this.cmbLabelShown ? isCmbLabAtCeil : isMaxLabAtCeil
            var hideFloor = this.cmbLabelShown
              ? isCmbLabAtFloor
              : isMinLabAtFloor

            if (hideCeil) {
              this.hideEl(this.ceilLab)
            } else if (!clHidden) {
              this.showEl(this.ceilLab)
            }

            // Hide or show floor label
            if (hideFloor) {
              this.hideEl(this.flrLab)
            } else if (!flHidden) {
              this.showEl(this.flrLab)
            }
          }
        },

        isLabelBelowFloorLab: function(label) {
          var isRTL = this.options.rightToLeft,
            pos = label.rzsp,
            dim = label.rzsd,
            floorPos = this.flrLab.rzsp,
            floorDim = this.flrLab.rzsd
          return isRTL
            ? pos + dim >= floorPos - 2
            : pos <= floorPos + floorDim + 2
        },

        isLabelAboveCeilLab: function(label) {
          var isRTL = this.options.rightToLeft,
            pos = label.rzsp,
            dim = label.rzsd,
            ceilPos = this.ceilLab.rzsp,
            ceilDim = this.ceilLab.rzsd
          return isRTL ? pos <= ceilPos + ceilDim + 2 : pos + dim >= ceilPos - 2
        },

        /**
         * Update restricted area bar
         *
         * @returns {undefined}
         */
        updateRestrictionBar: function() {
          var position = 0,
            dimension = 0
          if (this.options.restrictedRange) {
            this.options.restrictedRange = !Array.isArray(
              this.options.restrictedRange
            )
              ? [this.options.restrictedRange]
              : this.options.restrictedRange
            for (var i in this.options.restrictedRange) {
              var from = this.valueToPosition(
                  this.options.restrictedRange[i].from
                ),
                to = this.valueToPosition(this.options.restrictedRange[i].to)
              dimension = Math.abs(to - from)
              position = this.options.rightToLeft
                ? to + this.handleHalfDim
                : from + this.handleHalfDim
              this.setDimension(this.restrictedBar[i], dimension)
              this.setPosition(this.restrictedBar[i], position)
            }
          }
        },

        /**
         * Update slider selection bar, combined label and range label
         *
         * @returns {undefined}
         */
        updateSelectionBar: function() {
          var position = 0,
            dimension = 0,
            isSelectionBarFromRight = this.options.rightToLeft
              ? !this.options.showSelectionBarEnd
              : this.options.showSelectionBarEnd,
            positionForRange = this.options.rightToLeft
              ? this.maxH.rzsp + this.handleHalfDim
              : this.minH.rzsp + this.handleHalfDim

          if (this.range) {
            dimension = Math.abs(this.maxH.rzsp - this.minH.rzsp)
            position = positionForRange
          } else {
            if (this.options.showSelectionBarFromValue !== null) {
              var center = this.options.showSelectionBarFromValue,
                centerPosition = this.valueToPosition(center),
                isModelGreaterThanCenter = this.options.rightToLeft
                  ? this.lowValue <= center
                  : this.lowValue > center
              if (isModelGreaterThanCenter) {
                dimension = this.minH.rzsp - centerPosition
                position = centerPosition + this.handleHalfDim
              } else {
                dimension = centerPosition - this.minH.rzsp
                position = this.minH.rzsp + this.handleHalfDim
              }
            } else if (isSelectionBarFromRight) {
              dimension =
                Math.abs(this.maxPos - this.minH.rzsp) + this.handleHalfDim
              position = this.minH.rzsp + this.handleHalfDim
            } else {
              dimension = this.minH.rzsp + this.handleHalfDim
              position = 0
            }
          }
          this.setDimension(this.selBar, dimension)
          this.setPosition(this.selBar, position)
          if (this.range && this.options.showOuterSelectionBars) {
            if (this.options.rightToLeft) {
              this.setDimension(this.rightOutSelBar, position)
              this.setPosition(this.rightOutSelBar, 0)
              this.setDimension(
                this.leftOutSelBar,
                this.getDimension(this.fullBar) - (position + dimension)
              )
              this.setPosition(this.leftOutSelBar, position + dimension)
            } else {
              this.setDimension(this.leftOutSelBar, position)
              this.setPosition(this.leftOutSelBar, 0)
              this.setDimension(
                this.rightOutSelBar,
                this.getDimension(this.fullBar) - (position + dimension)
              )
              this.setPosition(this.rightOutSelBar, position + dimension)
            }
          }
          if (this.options.getSelectionBarColor) {
            var color = this.getSelectionBarColor()
            this.scope.barStyle = {
              backgroundColor: color,
            }
          } else if (this.options.selectionBarGradient) {
            var offset =
                this.options.showSelectionBarFromValue !== null
                  ? this.valueToPosition(this.options.showSelectionBarFromValue)
                  : 0,
              reversed = (offset - position > 0) ^ isSelectionBarFromRight,
              direction = this.options.vertical
                ? reversed
                  ? 'bottom'
                  : 'top'
                : reversed
                ? 'left'
                : 'right'
            this.scope.barStyle = {
              backgroundImage:
                'linear-gradient(to ' +
                direction +
                ', ' +
                this.options.selectionBarGradient.from +
                ' 0%,' +
                this.options.selectionBarGradient.to +
                ' 100%)',
            }
            if (this.options.vertical) {
              this.scope.barStyle.backgroundPosition =
                'center ' +
                (offset +
                  dimension +
                  position +
                  (reversed ? -this.handleHalfDim : 0)) +
                'px'
              this.scope.barStyle.backgroundSize =
                '100% ' + (this.barDimension - this.handleHalfDim) + 'px'
            } else {
              this.scope.barStyle.backgroundPosition =
                offset -
                position +
                (reversed ? this.handleHalfDim : 0) +
                'px center'
              this.scope.barStyle.backgroundSize =
                this.barDimension - this.handleHalfDim + 'px 100%'
            }
          }
        },

        /**
         * Wrapper around the getSelectionBarColor of the user to pass to
         * correct parameters
         */
        getSelectionBarColor: function() {
          if (this.range)
            return this.options.getSelectionBarColor(
              this.scope.rzSliderModel,
              this.scope.rzSliderHigh
            )
          return this.options.getSelectionBarColor(this.scope.rzSliderModel)
        },

        /**
         * Wrapper around the getPointerColor of the user to pass to
         * correct parameters
         */
        getPointerColor: function(pointerType) {
          if (pointerType === 'max') {
            return this.options.getPointerColor(
              this.scope.rzSliderHigh,
              pointerType
            )
          }
          return this.options.getPointerColor(
            this.scope.rzSliderModel,
            pointerType
          )
        },

        /**
         * Wrapper around the getTickColor of the user to pass to
         * correct parameters
         */
        getTickColor: function(value) {
          return this.options.getTickColor(value)
        },

        /**
         * Update combined label position and value
         *
         * @returns {undefined}
         */
        updateCmbLabel: function() {
          var isLabelOverlap = null
          if (this.options.rightToLeft) {
            isLabelOverlap =
              this.minLab.rzsp - this.minLab.rzsd - 10 <= this.maxLab.rzsp
          } else {
            isLabelOverlap =
              this.minLab.rzsp + this.minLab.rzsd + 10 >= this.maxLab.rzsp
          }

          if (isLabelOverlap) {
            var lowTr = this.getDisplayValue(this.lowValue, 'model'),
              highTr = this.getDisplayValue(this.highValue, 'high'),
              labelVal = ''
            if (this.options.mergeRangeLabelsIfSame && lowTr === highTr) {
              labelVal = lowTr
            } else {
              labelVal = this.options.rightToLeft
                ? highTr + this.options.labelOverlapSeparator + lowTr
                : lowTr + this.options.labelOverlapSeparator + highTr
            }

            this.translateFn(labelVal, this.cmbLab, 'cmb', false)
            var pos = this.options.boundPointerLabels
              ? Math.min(
                  Math.max(
                    this.selBar.rzsp +
                      this.selBar.rzsd / 2 -
                      this.cmbLab.rzsd / 2,
                    0
                  ),
                  this.barDimension - this.cmbLab.rzsd
                )
              : this.selBar.rzsp + this.selBar.rzsd / 2 - this.cmbLab.rzsd / 2

            this.setPosition(this.cmbLab, pos)
            this.cmbLabelShown = true
            this.hideEl(this.minLab)
            this.hideEl(this.maxLab)
            this.showEl(this.cmbLab)
          } else {
            this.cmbLabelShown = false
            this.updateHighHandle(this.valueToPosition(this.highValue))
            this.updateLowHandle(this.valueToPosition(this.lowValue))
            this.showEl(this.maxLab)
            this.showEl(this.minLab)
            this.hideEl(this.cmbLab)
          }
          if (this.options.autoHideLimitLabels) {
            this.shFloorCeil()
          }
        },

        /**
         * Return the translated value if a translate function is provided else the original value
         * @param value
         * @param which if it's min or max handle
         * @returns {*}
         */
        getDisplayValue: function(value, which) {
          if (this.options.stepsArray && !this.options.bindIndexForStepsArray) {
            value = this.getStepValue(value)
          }
          return this.customTrFn(value, this.options.id, which)
        },

        /**
         * Round value to step and precision based on minValue
         *
         * @param {number} value
         * @param {number} customStep a custom step to override the defined step
         * @returns {number}
         */
        roundStep: function(value, customStep) {
          var step = customStep ? customStep : this.step,
            steppedDifference = parseFloat(
              (value - this.minValue) / step
            ).toPrecision(12)
          steppedDifference = Math.round(+steppedDifference) * step
          var newValue = (this.minValue + steppedDifference).toFixed(
            this.precision
          )
          return +newValue
        },

        /**
         * Hide element
         *
         * @param element
         * @returns {jqLite} The jqLite wrapped DOM element
         */
        hideEl: function(element) {
          return element.css({
            visibility: 'hidden',
          })
        },

        /**
         * Show element
         *
         * @param element The jqLite wrapped DOM element
         * @returns {jqLite} The jqLite
         */
        showEl: function(element) {
          if (!!element.rzAlwaysHide) {
            return element
          }

          return element.css({
            visibility: 'visible',
          })
        },

        /**
         * Set element left/top position depending on whether slider is horizontal or vertical
         *
         * @param {jqLite} elem The jqLite wrapped DOM element
         * @param {number} pos
         * @returns {number}
         */
        setPosition: function(elem, pos) {
          elem.rzsp = pos
          var css = {}
          css[this.positionProperty] = Math.round(pos) + 'px'
          elem.css(css)
          return pos
        },

        resetPosition: function(elem) {
          elem.css({
            left: null,
            bottom: null,
          })
        },

        /**
         * Get element width/height depending on whether slider is horizontal or vertical
         *
         * @param {jqLite} elem The jqLite wrapped DOM element
         * @returns {number}
         */
        getDimension: function(elem) {
          var val = elem[0].getBoundingClientRect()
          if (this.options.vertical)
            elem.rzsd = (val.bottom - val.top) * this.options.scale
          else elem.rzsd = (val.right - val.left) * this.options.scale
          return elem.rzsd
        },

        /**
         * Set element width/height depending on whether slider is horizontal or vertical
         *
         * @param {jqLite} elem  The jqLite wrapped DOM element
         * @param {number} dim
         * @returns {number}
         */
        setDimension: function(elem, dim) {
          elem.rzsd = dim
          var css = {}
          css[this.dimensionProperty] = Math.round(dim) + 'px'
          elem.css(css)
          return dim
        },

        /**
         * Returns a value that is within slider range
         *
         * @param {number} val
         * @returns {number}
         */
        sanitizeValue: function(val) {
          return Math.min(Math.max(val, this.minValue), this.maxValue)
        },

        /**
         * Translate value to pixel position
         *
         * @param {number} val
         * @returns {number}
         */
        valueToPosition: function(val) {
          var fn = this.linearValueToPosition
          if (this.options.customValueToPosition)
            fn = this.options.customValueToPosition
          else if (this.options.logScale) fn = this.logValueToPosition

          val = this.sanitizeValue(val)
          var percent = fn(val, this.minValue, this.maxValue) || 0
          if (this.options.rightToLeft) percent = 1 - percent
          return percent * this.maxPos
        },

        linearValueToPosition: function(val, minVal, maxVal) {
          var range = maxVal - minVal
          return (val - minVal) / range
        },

        logValueToPosition: function(val, minVal, maxVal) {
          val = Math.log(val)
          minVal = Math.log(minVal)
          maxVal = Math.log(maxVal)
          var range = maxVal - minVal
          return (val - minVal) / range
        },

        /**
         * Translate position to model value
         *
         * @param {number} position
         * @returns {number}
         */
        positionToValue: function(position) {
          var percent = position / this.maxPos
          if (this.options.rightToLeft) percent = 1 - percent
          var fn = this.linearPositionToValue
          if (this.options.customPositionToValue)
            fn = this.options.customPositionToValue
          else if (this.options.logScale) fn = this.logPositionToValue
          return fn(percent, this.minValue, this.maxValue) || 0
        },

        linearPositionToValue: function(percent, minVal, maxVal) {
          return percent * (maxVal - minVal) + minVal
        },

        logPositionToValue: function(percent, minVal, maxVal) {
          minVal = Math.log(minVal)
          maxVal = Math.log(maxVal)
          var value = percent * (maxVal - minVal) + minVal
          return Math.exp(value)
        },

        getEventAttr: function(event, attr) {
          return event.originalEvent === undefined
            ? event[attr]
            : event.originalEvent[attr]
        },

        // Events
        /**
         * Get the X-coordinate or Y-coordinate of an event
         *
         * @param {Object} event  The event
         * @param targetTouchId The identifier of the touch with the X/Y coordinates
         * @returns {number}
         */
        getEventXY: function(event, targetTouchId) {
          /* http://stackoverflow.com/a/12336075/282882 */
          //noinspection JSLint
          var clientXY = this.options.vertical ? 'clientY' : 'clientX'
          if (event[clientXY] !== undefined) {
            return event[clientXY]
          }

          var touches = this.getEventAttr(event, 'touches')

          if (targetTouchId !== undefined) {
            for (var i = 0; i < touches.length; i++) {
              if (touches[i].identifier === targetTouchId) {
                return touches[i][clientXY]
              }
            }
          }

          // If no target touch or the target touch was not found in the event
          // returns the coordinates of the first touch
          return touches[0][clientXY]
        },

        /**
         * Compute the event position depending on whether the slider is horizontal or vertical
         * @param event
         * @param targetTouchId If targetTouchId is provided it will be considered the position of that
         * @returns {number}
         */
        getEventPosition: function(event, targetTouchId) {
          var sliderPos = this.sliderElem.rzsp,
            eventPos = 0
          if (this.options.vertical)
            eventPos = -this.getEventXY(event, targetTouchId) + sliderPos
          else eventPos = this.getEventXY(event, targetTouchId) - sliderPos
          return eventPos * this.options.scale - this.handleHalfDim // #346 handleHalfDim is already scaled
        },

        /**
         * Get event names for move and event end
         *
         * @param {Event}    event    The event
         *
         * @return {{moveEvent: string, endEvent: string}}
         */
        getEventNames: function(event) {
          var eventNames = {
            moveEvent: '',
            endEvent: '',
          }

          if (this.getEventAttr(event, 'touches')) {
            eventNames.moveEvent = 'touchmove'
            eventNames.endEvent = 'touchend'
          } else {
            eventNames.moveEvent = 'mousemove'
            eventNames.endEvent = 'mouseup'
          }

          return eventNames
        },

        /**
         * Get the handle closest to an event.
         *
         * @param event {Event} The event
         * @returns {jqLite} The handle closest to the event.
         */
        getNearestHandle: function(event) {
          if (!this.range) {
            return this.minH
          }
          var position = this.getEventPosition(event),
            distanceMin = Math.abs(position - this.minH.rzsp),
            distanceMax = Math.abs(position - this.maxH.rzsp)
          if (distanceMin < distanceMax) return this.minH
          else if (distanceMin > distanceMax) return this.maxH
          else if (!this.options.rightToLeft)
            //if event is at the same distance from min/max then if it's at left of minH, we return minH else maxH
            return position < this.minH.rzsp ? this.minH : this.maxH
          //reverse in rtl
          else return position > this.minH.rzsp ? this.minH : this.maxH
        },

        /**
         * Wrapper function to focus an angular element
         *
         * @param el {AngularElement} the element to focus
         */
        focusElement: function(el) {
          var DOM_ELEMENT = 0
          el[DOM_ELEMENT].focus()
        },

        /**
         * Bind mouse and touch events to slider handles
         *
         * @returns {undefined}
         */
        bindEvents: function() {
          var barTracking, barStart, barMove

          if (this.options.draggableRange) {
            barTracking = 'rzSliderDrag'
            barStart = this.onDragStart
            barMove = this.onDragMove
          } else {
            barTracking = 'lowValue'
            barStart = this.onStart
            barMove = this.onMove
          }

          if (!this.options.onlyBindHandles) {
            this.selBar.on(
              'mousedown',
              angular.bind(this, barStart, null, barTracking)
            )
            this.selBar.on(
              'mousedown',
              angular.bind(this, barMove, this.selBar)
            )
          }

          if (this.options.draggableRangeOnly) {
            this.minH.on(
              'mousedown',
              angular.bind(this, barStart, null, barTracking)
            )
            this.maxH.on(
              'mousedown',
              angular.bind(this, barStart, null, barTracking)
            )
          } else {
            this.minH.on(
              'mousedown',
              angular.bind(this, this.onStart, this.minH, 'lowValue')
            )
            if (this.range) {
              this.maxH.on(
                'mousedown',
                angular.bind(this, this.onStart, this.maxH, 'highValue')
              )
            }
            if (!this.options.onlyBindHandles) {
              this.fullBar.on(
                'mousedown',
                angular.bind(this, this.onStart, null, null)
              )
              this.fullBar.on(
                'mousedown',
                angular.bind(this, this.onMove, this.fullBar)
              )
              this.ticks.on(
                'mousedown',
                angular.bind(this, this.onStart, null, null)
              )
              this.ticks.on(
                'mousedown',
                angular.bind(this, this.onTickClick, this.ticks)
              )
            }
          }

          if (!this.options.onlyBindHandles) {
            this.selBar.on(
              'touchstart',
              angular.bind(this, barStart, null, barTracking)
            )
            this.selBar.on(
              'touchstart',
              angular.bind(this, barMove, this.selBar)
            )
          }
          if (this.options.draggableRangeOnly) {
            this.minH.on(
              'touchstart',
              angular.bind(this, barStart, null, barTracking)
            )
            this.maxH.on(
              'touchstart',
              angular.bind(this, barStart, null, barTracking)
            )
          } else {
            this.minH.on(
              'touchstart',
              angular.bind(this, this.onStart, this.minH, 'lowValue')
            )
            if (this.range) {
              this.maxH.on(
                'touchstart',
                angular.bind(this, this.onStart, this.maxH, 'highValue')
              )
            }
            if (!this.options.onlyBindHandles) {
              this.fullBar.on(
                'touchstart',
                angular.bind(this, this.onStart, null, null)
              )
              this.fullBar.on(
                'touchstart',
                angular.bind(this, this.onMove, this.fullBar)
              )
              this.ticks.on(
                'touchstart',
                angular.bind(this, this.onStart, null, null)
              )
              this.ticks.on(
                'touchstart',
                angular.bind(this, this.onTickClick, this.ticks)
              )
            }
          }

          if (this.options.keyboardSupport) {
            this.minH.on(
              'focus',
              angular.bind(this, this.onPointerFocus, this.minH, 'lowValue')
            )
            if (this.range) {
              this.maxH.on(
                'focus',
                angular.bind(this, this.onPointerFocus, this.maxH, 'highValue')
              )
            }
          }
        },

        /**
         * Unbind mouse and touch events to slider handles
         *
         * @returns {undefined}
         */
        unbindEvents: function() {
          this.minH.off()
          this.maxH.off()
          this.fullBar.off()
          this.selBar.off()
          this.ticks.off()
        },

        /**
         * onStart event handler
         *
         * @param {?Object} pointer The jqLite wrapped DOM element; if null, the closest handle is used
         * @param {?string} ref     The name of the handle being changed; if null, the closest handle's value is modified
         * @param {Event}   event   The event
         * @returns {undefined}
         */
        onStart: function(pointer, ref, event) {
          var ehMove,
            ehEnd,
            eventNames = this.getEventNames(event)

          event.stopPropagation()
          event.preventDefault()

          // We have to do this in case the HTML where the sliders are on
          // have been animated into view.
          this.calcViewDimensions()

          if (pointer) {
            this.tracking = ref
          } else {
            pointer = this.getNearestHandle(event)
            this.tracking = pointer === this.minH ? 'lowValue' : 'highValue'
          }

          pointer.addClass('rz-active')

          if (this.options.keyboardSupport) this.focusElement(pointer)

          ehMove = angular.bind(
            this,
            this.dragging.active ? this.onDragMove : this.onMove,
            pointer
          )
          ehEnd = angular.bind(this, this.onEnd, ehMove)

          $document.on(eventNames.moveEvent, ehMove)
          $document.on(eventNames.endEvent, ehEnd)
          this.endHandlerToBeRemovedOnEnd = ehEnd

          this.callOnStart()

          var changedTouches = this.getEventAttr(event, 'changedTouches')
          if (changedTouches) {
            // Store the touch identifier
            if (!this.touchId) {
              this.isDragging = true
              this.touchId = changedTouches[0].identifier
            }
          }
        },

        /**
         * onMove event handler
         *
         * @param {jqLite} pointer
         * @param {Event}  event The event
         * @param {boolean}  fromTick if the event occured on a tick or not
         * @returns {undefined}
         */
        onMove: function(pointer, event, fromTick) {
          if (!this.options.disableAnimation) {
            if (this.moving) {
              this.sliderElem.addClass('noanimate')
            }
          }
          this.moving = true
          var changedTouches = this.getEventAttr(event, 'changedTouches')
          var touchForThisSlider
          if (changedTouches) {
            for (var i = 0; i < changedTouches.length; i++) {
              if (changedTouches[i].identifier === this.touchId) {
                touchForThisSlider = changedTouches[i]
                break
              }
            }
          }

          if (changedTouches && !touchForThisSlider) {
            return
          }

          var newPos = this.getEventPosition(
              event,
              touchForThisSlider ? touchForThisSlider.identifier : undefined
            ),
            newValue,
            ceilValue = this.options.rightToLeft
              ? this.minValue
              : this.maxValue,
            flrValue = this.options.rightToLeft ? this.maxValue : this.minValue

          if (newPos <= 0) {
            newValue = flrValue
          } else if (newPos >= this.maxPos) {
            newValue = ceilValue
          } else {
            newValue = this.positionToValue(newPos)
            if (fromTick && angular.isNumber(this.options.showTicks))
              newValue = this.roundStep(newValue, this.options.showTicks)
            else newValue = this.roundStep(newValue)
          }
          this.positionTrackingHandle(newValue)
        },

        /**
         * onEnd event handler
         *
         * @param {Event}    event    The event
         * @param {Function} ehMove   The bound move event handler
         * @returns {undefined}
         */
        onEnd: function(ehMove, event) {
          this.moving = false
          if (!this.options.disableAnimation) {
            this.sliderElem.removeClass('noanimate')
          }
          var changedTouches = this.getEventAttr(event, 'changedTouches')
          if (changedTouches && changedTouches[0].identifier !== this.touchId) {
            return
          }
          this.isDragging = false
          this.touchId = null

          if (!this.options.keyboardSupport) {
            this.minH.removeClass('rz-active')
            this.maxH.removeClass('rz-active')
            this.tracking = ''
          }
          this.dragging.active = false

          var eventName = this.getEventNames(event)
          $document.off(eventName.moveEvent, ehMove)
          $document.off(eventName.endEvent, this.endHandlerToBeRemovedOnEnd)
          this.endHandlerToBeRemovedOnEnd = null
          this.callOnEnd()
        },

        onTickClick: function(pointer, event) {
          this.onMove(pointer, event, true)
        },

        onPointerFocus: function(pointer, ref) {
          this.tracking = ref
          pointer.one('blur', angular.bind(this, this.onPointerBlur, pointer))
          pointer.on('keydown', angular.bind(this, this.onKeyboardEvent))
          pointer.on('keyup', angular.bind(this, this.onKeyUp))
          this.firstKeyDown = true
          pointer.addClass('rz-active')

          this.currentFocusElement = {
            pointer: pointer,
            ref: ref,
          }
        },

        onKeyUp: function() {
          this.firstKeyDown = true
          this.callOnEnd()
        },

        onPointerBlur: function(pointer) {
          pointer.off('keydown')
          pointer.off('keyup')
          pointer.removeClass('rz-active')
          if (!this.isDragging) {
            this.tracking = ''
            this.currentFocusElement = null
          }
        },

        /**
         * Skip restricted range function when arrow keys use
         *
         * @param {number} currentValue value of the slider
         * @param {number} key arrow key used
         *
         * @returns {number} currentValue value of the slider
         */

        skipRestrictedRanges: function(key, currentValue) {
          if (
            this.options.restrictedRange &&
            Array.isArray(this.options.restrictedRange)
          ) {
            for (var i in this.options.restrictedRange) {
              var range = this.options.restrictedRange[i]
              // if it first or last value
              if (
                (range.from === 0 &&
                  currentValue === 0 &&
                  [37, 40].includes(key)) || // LEFT or DOWN
                (range.to >=
                  this.options.restrictedRange[
                    this.options.restrictedRange.length - 1
                  ].to &&
                  currentValue >=
                    this.options.restrictedRange[
                      this.options.restrictedRange.length - 1
                    ].to &&
                  [38, 39].includes(key)) // UP or RIGHT
              ) {
                return currentValue
              }
              if (range.to > currentValue && currentValue > range.from) {
                if (
                  Math.abs(range.to - currentValue) >
                  Math.abs(range.from - currentValue)
                ) {
                  currentValue = range.to
                } else {
                  currentValue = range.from
                }
              }
            }
          }

          return currentValue
        },

        /**
         * Key actions helper function
         *
         * @param {number} currentValue value of the slider
         *
         * @returns {?Object} action value mappings
         */
        getKeyActions: function(currentValue) {
          var increaseStep = currentValue + this.step,
            decreaseStep = currentValue - this.step,
            increasePage = currentValue + this.valueRange / 10,
            decreasePage = currentValue - this.valueRange / 10

          if (this.options.reversedControls) {
            increaseStep = currentValue - this.step
            decreaseStep = currentValue + this.step
            increasePage = currentValue - this.valueRange / 10
            decreasePage = currentValue + this.valueRange / 10
          }

          //Left to right default actions
          var actions = {
            UP: increaseStep,
            DOWN: decreaseStep,
            LEFT: decreaseStep,
            RIGHT: increaseStep,
            PAGEUP: increasePage,
            PAGEDOWN: decreasePage,
            HOME: this.options.reversedControls ? this.maxValue : this.minValue,
            END: this.options.reversedControls ? this.minValue : this.maxValue,
          }
          //right to left means swapping right and left arrows
          if (this.options.rightToLeft) {
            actions.LEFT = increaseStep
            actions.RIGHT = decreaseStep
            // right to left and vertical means we also swap up and down
            if (this.options.vertical) {
              actions.UP = decreaseStep
              actions.DOWN = increaseStep
            }
          }
          return actions
        },

        onKeyboardEvent: function(event) {
          var keyCode = event.keyCode || event.which
          var currentValue = this[this.tracking]
          var keys = {
              38: 'UP',
              40: 'DOWN',
              37: 'LEFT',
              39: 'RIGHT',
              33: 'PAGEUP',
              34: 'PAGEDOWN',
              36: 'HOME',
              35: 'END',
            },
            actions = this.getKeyActions(currentValue),
            key = keys[keyCode],
            action = actions[key]
          if (action == null || this.tracking === '') return
          event.preventDefault()

          if (this.firstKeyDown) {
            this.firstKeyDown = false
            this.callOnStart()
          }

          var self = this
          $timeout(function() {
            var newValue = self.roundStep(self.sanitizeValue(action))
            newValue = self.options.skipRestrictedRangesWithArrowKeys
              ? self.skipRestrictedRanges(keyCode, newValue)
              : newValue
            if (!self.options.draggableRangeOnly) {
              self.positionTrackingHandle(newValue)
            } else {
              var difference = self.highValue - self.lowValue,
                newMinValue,
                newMaxValue
              if (self.tracking === 'lowValue') {
                newMinValue = newValue
                newMaxValue = newValue + difference
                if (newMaxValue > self.maxValue) {
                  newMaxValue = self.maxValue
                  newMinValue = newMaxValue - difference
                }
              } else {
                newMaxValue = newValue
                newMinValue = newValue - difference
                if (newMinValue < self.minValue) {
                  newMinValue = self.minValue
                  newMaxValue = newMinValue + difference
                }
              }
              self.positionTrackingBar(newMinValue, newMaxValue)
            }
          })
        },

        /**
         * onDragStart event handler
         *
         * Handles dragging of the middle bar.
         *
         * @param {Object} pointer The jqLite wrapped DOM element
         * @param {string} ref     One of the refLow, refHigh values
         * @param {Event}  event   The event
         * @returns {undefined}
         */
        onDragStart: function(pointer, ref, event) {
          var position = this.getEventPosition(event)
          this.dragging = {
            active: true,
            value: this.positionToValue(position),
            difference: this.highValue - this.lowValue,
            lowLimit: this.options.rightToLeft
              ? this.minH.rzsp - position
              : position - this.minH.rzsp,
            highLimit: this.options.rightToLeft
              ? position - this.maxH.rzsp
              : this.maxH.rzsp - position,
          }

          this.onStart(pointer, ref, event)
        },

        /**
         * getValue helper function
         *
         * gets max or min value depending on whether the newPos is outOfBounds above or below the bar and rightToLeft
         *
         * @param {string} type 'max' || 'min' The value we are calculating
         * @param {number} newPos  The new position
         * @param {boolean} outOfBounds Is the new position above or below the max/min?
         * @param {boolean} isAbove Is the new position above the bar if out of bounds?
         *
         * @returns {number}
         */
        getValue: function(type, newPos, outOfBounds, isAbove) {
          var isRTL = this.options.rightToLeft,
            value = null

          if (type === 'min') {
            if (outOfBounds) {
              if (isAbove) {
                value = isRTL
                  ? this.minValue
                  : this.maxValue - this.dragging.difference
              } else {
                value = isRTL
                  ? this.maxValue - this.dragging.difference
                  : this.minValue
              }
            } else {
              value = isRTL
                ? this.positionToValue(newPos + this.dragging.lowLimit)
                : this.positionToValue(newPos - this.dragging.lowLimit)
            }
          } else {
            if (outOfBounds) {
              if (isAbove) {
                value = isRTL
                  ? this.minValue + this.dragging.difference
                  : this.maxValue
              } else {
                value = isRTL
                  ? this.maxValue
                  : this.minValue + this.dragging.difference
              }
            } else {
              if (isRTL) {
                value =
                  this.positionToValue(newPos + this.dragging.lowLimit) +
                  this.dragging.difference
              } else {
                value =
                  this.positionToValue(newPos - this.dragging.lowLimit) +
                  this.dragging.difference
              }
            }
          }
          return this.roundStep(value)
        },

        /**
         * onDragMove event handler
         *
         * Handles dragging of the middle bar.
         *
         * @param {jqLite} pointer
         * @param {Event}  event The event
         * @returns {undefined}
         */
        onDragMove: function(pointer, event) {
          if (!this.options.disableAnimation) {
            if (this.moving) {
              this.sliderElem.addClass('noanimate')
            }
          }
          this.moving = true
          var newPos = this.getEventPosition(event),
            newMinValue,
            newMaxValue,
            ceilLimit,
            flrLimit,
            isUnderFlrLimit,
            isOverCeilLimit,
            flrH,
            ceilH

          if (this.options.rightToLeft) {
            ceilLimit = this.dragging.lowLimit
            flrLimit = this.dragging.highLimit
            flrH = this.maxH
            ceilH = this.minH
          } else {
            ceilLimit = this.dragging.highLimit
            flrLimit = this.dragging.lowLimit
            flrH = this.minH
            ceilH = this.maxH
          }
          isUnderFlrLimit = newPos <= flrLimit
          isOverCeilLimit = newPos >= this.maxPos - ceilLimit

          if (isUnderFlrLimit) {
            if (flrH.rzsp === 0) return
            newMinValue = this.getValue('min', newPos, true, false)
            newMaxValue = this.getValue('max', newPos, true, false)
          } else if (isOverCeilLimit) {
            if (ceilH.rzsp === this.maxPos) return
            newMaxValue = this.getValue('max', newPos, true, true)
            newMinValue = this.getValue('min', newPos, true, true)
          } else {
            newMinValue = this.getValue('min', newPos, false)
            newMaxValue = this.getValue('max', newPos, false)
          }
          this.positionTrackingBar(newMinValue, newMaxValue)
        },

        /**
         * Set the new value and position for the entire bar
         *
         * @param {number} newMinValue   the new minimum value
         * @param {number} newMaxValue   the new maximum value
         */
        positionTrackingBar: function(newMinValue, newMaxValue) {
          if (
            this.options.minLimit != null &&
            newMinValue < this.options.minLimit
          ) {
            newMinValue = this.options.minLimit
            newMaxValue = newMinValue + this.dragging.difference
          }
          if (
            this.options.maxLimit != null &&
            newMaxValue > this.options.maxLimit
          ) {
            newMaxValue = this.options.maxLimit
            newMinValue = newMaxValue - this.dragging.difference
          }

          this.lowValue = newMinValue
          this.highValue = newMaxValue
          this.applyLowValue()
          if (this.range) this.applyHighValue()
          this.applyModel(true)
          this.updateHandles('lowValue', this.valueToPosition(newMinValue))
          this.updateHandles('highValue', this.valueToPosition(newMaxValue))
        },

        /**
         * Set the new value and position to the current tracking handle
         *
         * @param {number} newValue new model value
         */
        positionTrackingHandle: function(newValue) {
          var valueChanged = false
          newValue = this.applyMinMaxLimit(newValue)
          newValue = this.applyRestrictedRange(newValue)
          if (this.range) {
            if (this.options.pushRange) {
              newValue = this.applyPushRange(newValue)
              valueChanged = true
            } else {
              if (this.options.noSwitching) {
                if (this.tracking === 'lowValue' && newValue > this.highValue)
                  newValue = this.applyMinMaxRange(this.highValue)
                else if (
                  this.tracking === 'highValue' &&
                  newValue < this.lowValue
                )
                  newValue = this.applyMinMaxRange(this.lowValue)
              }
              newValue = this.applyMinMaxRange(newValue)
              /* This is to check if we need to switch the min and max handles */
              if (this.tracking === 'lowValue' && newValue > this.highValue) {
                this.lowValue = this.highValue
                this.applyLowValue()
                this.applyModel()
                this.updateHandles(this.tracking, this.maxH.rzsp)
                this.updateAriaAttributes()
                this.tracking = 'highValue'
                this.minH.removeClass('rz-active')
                this.maxH.addClass('rz-active')
                if (this.options.keyboardSupport) this.focusElement(this.maxH)
                valueChanged = true
              } else if (
                this.tracking === 'highValue' &&
                newValue < this.lowValue
              ) {
                this.highValue = this.lowValue
                this.applyHighValue()
                this.applyModel()
                this.updateHandles(this.tracking, this.minH.rzsp)
                this.updateAriaAttributes()
                this.tracking = 'lowValue'
                this.maxH.removeClass('rz-active')
                this.minH.addClass('rz-active')
                if (this.options.keyboardSupport) this.focusElement(this.minH)
                valueChanged = true
              }
            }
          }

          if (this[this.tracking] !== newValue) {
            this[this.tracking] = newValue
            if (this.tracking === 'lowValue') this.applyLowValue()
            else this.applyHighValue()
            this.applyModel()
            this.updateHandles(this.tracking, this.valueToPosition(newValue))
            this.updateAriaAttributes()
            valueChanged = true
          }

          if (valueChanged) this.applyModel(true)
        },

        applyMinMaxLimit: function(newValue) {
          if (this.options.minLimit != null && newValue < this.options.minLimit)
            return this.options.minLimit
          if (this.options.maxLimit != null && newValue > this.options.maxLimit)
            return this.options.maxLimit
          return newValue
        },

        applyMinMaxRange: function(newValue) {
          var oppositeValue =
              this.tracking === 'lowValue' ? this.highValue : this.lowValue,
            difference = Math.abs(newValue - oppositeValue)
          if (this.options.minRange != null) {
            if (difference < this.options.minRange) {
              if (this.tracking === 'lowValue')
                return this.highValue - this.options.minRange
              else return this.lowValue + this.options.minRange
            }
          }
          if (this.options.maxRange != null) {
            if (difference > this.options.maxRange) {
              if (this.tracking === 'lowValue')
                return this.highValue - this.options.maxRange
              else return this.lowValue + this.options.maxRange
            }
          }
          return newValue
        },

        applyRestrictedRange: function(newValue) {
          for (var i in this.options.restrictedRange) {
            if (
              this.options.restrictedRange[i] != null &&
              newValue > this.options.restrictedRange[i].from &&
              newValue < this.options.restrictedRange[i].to
            ) {
              var halfWidth =
                (this.options.restrictedRange[i].to -
                  this.options.restrictedRange[i].from) /
                2
              if (this.tracking === 'lowValue') {
                return newValue >
                  this.options.restrictedRange[i].from + halfWidth
                  ? this.options.restrictedRange[i].to
                  : this.options.restrictedRange[i].from
              }
              if (this.tracking === 'highValue') {
                return newValue < this.options.restrictedRange[i].to - halfWidth
                  ? this.options.restrictedRange[i].from
                  : this.options.restrictedRange[i].to
              }
            }
          }

          return newValue
        },

        applyPushRange: function(newValue) {
          var difference =
              this.tracking === 'lowValue'
                ? this.highValue - newValue
                : newValue - this.lowValue,
            minRange =
              this.options.minRange !== null
                ? this.options.minRange
                : this.options.step,
            maxRange = this.options.maxRange
          // if smaller than minRange
          if (difference < minRange) {
            if (this.tracking === 'lowValue') {
              this.highValue = Math.min(newValue + minRange, this.maxValue)
              newValue = this.highValue - minRange
              this.applyHighValue()
              this.updateHandles(
                'highValue',
                this.valueToPosition(this.highValue)
              )
            } else {
              this.lowValue = Math.max(newValue - minRange, this.minValue)
              newValue = this.lowValue + minRange
              this.applyLowValue()
              this.updateHandles(
                'lowValue',
                this.valueToPosition(this.lowValue)
              )
            }
            this.updateAriaAttributes()
          } else if (maxRange !== null && difference > maxRange) {
            // if greater than maxRange
            if (this.tracking === 'lowValue') {
              this.highValue = newValue + maxRange
              this.applyHighValue()
              this.updateHandles(
                'highValue',
                this.valueToPosition(this.highValue)
              )
            } else {
              this.lowValue = newValue - maxRange
              this.applyLowValue()
              this.updateHandles(
                'lowValue',
                this.valueToPosition(this.lowValue)
              )
            }
            this.updateAriaAttributes()
          }
          return newValue
        },

        /**
         * Apply the model values using scope.$apply.
         * We wrap it with the internalChange flag to avoid the watchers to be called
         */
        applyModel: function(callOnChange) {
          this.internalChange = true
          this.scope.$apply()
          callOnChange && this.callOnChange()
          this.internalChange = false
        },

        /**
         * Call the onStart callback if defined
         * The callback call is wrapped in a $evalAsync to ensure that its result will be applied to the scope.
         *
         * @returns {undefined}
         */
        callOnStart: function() {
          if (this.options.onStart) {
            var self = this,
              pointerType = this.tracking === 'lowValue' ? 'min' : 'max'
            this.scope.$evalAsync(function() {
              self.options.onStart(
                self.options.id,
                self.scope.rzSliderModel,
                self.scope.rzSliderHigh,
                pointerType
              )
            })
          }
        },

        /**
         * Call the onChange callback if defined
         * The callback call is wrapped in a $evalAsync to ensure that its result will be applied to the scope.
         *
         * @returns {undefined}
         */
        callOnChange: function() {
          if (this.options.onChange) {
            var self = this,
              pointerType = this.tracking === 'lowValue' ? 'min' : 'max'
            this.scope.$evalAsync(function() {
              self.options.onChange(
                self.options.id,
                self.scope.rzSliderModel,
                self.scope.rzSliderHigh,
                pointerType
              )
            })
          }
        },

        /**
         * Call the onEnd callback if defined
         * The callback call is wrapped in a $evalAsync to ensure that its result will be applied to the scope.
         *
         * @returns {undefined}
         */
        callOnEnd: function() {
          if (this.options.onEnd) {
            var self = this,
              pointerType = this.tracking === 'lowValue' ? 'min' : 'max'
            this.scope.$evalAsync(function() {
              self.options.onEnd(
                self.options.id,
                self.scope.rzSliderModel,
                self.scope.rzSliderHigh,
                pointerType
              )
            })
          }
          this.scope.$emit('slideEnded')
        },
      }

      return Slider
    })
    .directive('rzslider', function(RzSlider) {
      'use strict'

      return {
        restrict: 'AE',
        replace: true,
        scope: {
          rzSliderModel: '=?',
          rzSliderHigh: '=?',
          rzSliderOptions: '&?',
          rzSliderTplUrl: '@',
        },

        /**
         * Return template URL
         *
         * @param {jqLite} elem
         * @param {Object} attrs
         * @return {string}
         */
        templateUrl: function(elem, attrs) {
          //noinspection JSUnresolvedVariable
          return attrs.rzSliderTplUrl || 'rzSliderTpl.html'
        },

        link: function(scope, elem) {
          scope.slider = new RzSlider(scope, elem) //attach on scope so we can test it
        },
      }
    })

  // IDE assist

  /**
   * @name ngScope
   *
   * @property {number} rzSliderModel
   * @property {number} rzSliderHigh
   * @property {Object} rzSliderOptions
   */

  /**
   * @name jqLite
   *
   * @property {number|undefined} rzsp rzslider label position position
   * @property {number|undefined} rzsd rzslider element dimension
   * @property {string|undefined} rzsv rzslider label value/text
   * @property {Function} css
   * @property {Function} text
   */

  /**
   * @name Event
   * @property {Array} touches
   * @property {Event} originalEvent
   */

  /**
   * @name ThrottleOptions
   *
   * @property {boolean} leading
   * @property {boolean} trailing
   */

  /*templateReplacement*/

  return module.name
})
